package org.unidata.mdm.rest.v1.meta.service.systems;

import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.unidata.mdm.core.type.model.SourceSystemElement;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.context.GetSourceSystemsContext;
import org.unidata.mdm.meta.context.UpsertSourceSystemsContext;
import org.unidata.mdm.meta.type.instance.SourceSystemsInstance;
import org.unidata.mdm.meta.type.model.sourcesystem.SourceSystemsModel;
import org.unidata.mdm.rest.v1.meta.converter.GenericModelInfoConverter;
import org.unidata.mdm.rest.v1.meta.converter.SourceSystemConverter;
import org.unidata.mdm.rest.v1.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.rest.v1.meta.ro.GetGenericModelInfoResultRO;
import org.unidata.mdm.rest.v1.meta.ro.UpsertGenericModelInfoRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.system.DeleteSystemResultRO;
import org.unidata.mdm.rest.v1.meta.ro.system.GetSystemResultRO;
import org.unidata.mdm.rest.v1.meta.ro.system.GetSystemsResultRO;
import org.unidata.mdm.rest.v1.meta.ro.system.ImportSystemResultRO;
import org.unidata.mdm.rest.v1.meta.ro.system.SourceSystemRO;
import org.unidata.mdm.rest.v1.meta.ro.system.UpsertSystemRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.system.UpsertSystemResultRO;
import org.unidata.mdm.rest.v1.meta.service.AbstractMetaModelRestService;
import org.unidata.mdm.system.exception.PlatformBusinessException;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * Source system rest controller
 *
 * @author Alexandr Serov
 * @since 23.11.2020
 **/
@Consumes({"application/json"})
@Produces({"application/json"})
@Path(SourceSystemRestService.SERVICE_PATH)
public class SourceSystemRestService extends AbstractMetaModelRestService {

    public static final String SERVICE_PATH = "source-systems";

    private static final String SERVICE_TAG = "source-systems";

    private static final String SYSTEM_NOT_FOUND_ERROR = "System source %s not found";

    /**
     * Load and return list of all source systems.
     *
     * @return list of source systems. {@see SourceSystemsRO}.
     */
    @GET
    @Operation(
        description = "Get all source systems.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG
    )
    public GetSystemsResultRO findAll() {
        GetSystemsResultRO result = new GetSystemsResultRO();
        SourceSystemsInstance systems = instance(Descriptors.SOURCE_SYSTEMS);
        SourceSystemElement adminSystem = systems.getAdminElement();
        Collection<SourceSystemElement> allSystems = ObjectUtils.defaultIfNull(systems.getSourceSystems(), Collections.emptyList());
        result.setSourceSystems(allSystems.stream().map(SourceSystemConverter::to)
            .filter(Objects::nonNull)
            .sorted((o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()))
            .collect(Collectors.toList()));
        if (adminSystem != null) {
            result.setAdminSystemName(adminSystem.getName());
        }
        return result;
    }

    @GET
    @Path("{sourceSystemName}")
    @Operation(
        description = "Get system by name.",
        method = HttpMethod.DELETE,
        tags = SERVICE_TAG
    )
    public GetSystemResultRO getByName(@Parameter(description = "Source system name.", in = ParameterIn.PATH)
                                       @PathParam(value = "sourceSystemName") String sourceSystemName) {
        SourceSystemElement system = sourceSystemByName(sourceSystemName);
        GetSystemResultRO result = new GetSystemResultRO();
        result.setSourceSystem(SourceSystemConverter.to(system));
        return result;
    }

    @GET
    @Path("/export")
    @Produces(MediaType.TEXT_XML)
    @Operation(
        description = "Export source system model.",
        method = HttpMethod.GET,
        parameters = {@Parameter(description = "Storage ID. Optional", in = ParameterIn.QUERY, name = "storageId")},
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = StreamingOutput.class)), responseCode = "200")
        }, tags = SERVICE_TAG)
    public Response exportSourceSystems(@QueryParam("storageId") String storageId) {
        return exportXmlFile(SERVICE_TAG, instance(Descriptors.SOURCE_SYSTEMS, storageId).toSource());
    }


    @POST
    @Path("/import")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Operation(
        description = "Import source system model. Only xml is supported.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA), description = "Source file."),
        tags = SERVICE_TAG)
    public ImportSystemResultRO importSourceSystems(@Multipart("file") Attachment attachment) {
        return importXmlFile(attachment, SourceSystemsModel.class, sourceSystemsModel -> {
                ImportSystemResultRO result = new ImportSystemResultRO();
                result.setInstanceId(sourceSystemsModel.getInstanceId());
                result.setStorageId(sourceSystemsModel.getStorageId());
                metaModelService.upsert(UpsertSourceSystemsContext.builder()
                    .update(sourceSystemsModel.getValues())
                    .instanceId(sourceSystemsModel.getInstanceId())
                    .storageId(sourceSystemsModel.getStorageId())
                    .waitForFinish(true)
                    .build());
                return result;
            }
        );
    }

    /**
     * Creates or update source system.
     *
     * @param req Source system definition.
     * @return HTTP response.
     */
    @POST
    @Operation(
        description = "Create or update source system.", method = HttpMethod.POST, tags = SERVICE_TAG)
    public UpsertSystemResultRO upsert(final UpsertSystemRequestRO req) {
        SourceSystemRO sourceSystem = notNull("sourceSystem", req.getSourceSystem());
        UpsertSystemResultRO result = new UpsertSystemResultRO();
        metaModelService.upsert(UpsertSourceSystemsContext.builder()
            .update(SourceSystemConverter.from(sourceSystem))
            .build());
        result.setName(sourceSystem.getName());
//        result.setSystem(SourceSystemConverter.to(findSourceSystemByName(sourceSystem.getName())));
        return result;
    }

    @POST
    @Path("/info")
    @Operation(
        description = "Upsert generic source systems model info.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = UpsertGenericModelInfoRequestRO.class)),
            description = "Upsert request."),
        tags = SERVICE_TAG)
    public GetGenericModelInfoResultRO upsert(UpsertGenericModelInfoRequestRO req) {

        metaModelService.upsert(UpsertSourceSystemsContext.builder()
                .name(req.getName())
                .displayName(req.getDisplayName())
                .description(req.getDescription())
                .waitForFinish(true)
                .build());

        return GenericModelInfoConverter.to(metaModelService.get(GetSourceSystemsContext.builder()
                .modelInfo(true)
                .build()));
    }

    @GET
    @Path("/info")
    @Operation(
        description = "Gets generic source systems model info.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetGenericModelInfoResultRO get() {
        return GenericModelInfoConverter.to(metaModelService.get(GetSourceSystemsContext.builder()
                .modelInfo(true)
                .build()));
    }

    @DELETE
    @Path("{sourceSystemName}")
    @Operation(
        description = "Removes an existing source system.",
        method = HttpMethod.DELETE,
        tags = SERVICE_TAG
    )
    public DeleteSystemResultRO delete(@Parameter(description = "Source system name.", in = ParameterIn.PATH)
                                       @PathParam(value = "sourceSystemName") String sourceSystemName) {
        SourceSystemElement system = sourceSystemByName(sourceSystemName);
        DeleteSystemResultRO result = new DeleteSystemResultRO();
        metaModelService.upsert(UpsertSourceSystemsContext.builder()
            .delete(sourceSystemName)
            .build());
        result.setId(sourceSystemName);
        return result;
    }

    private SourceSystemElement findSourceSystemByName(String systemName) {
        return instance(Descriptors.SOURCE_SYSTEMS).getSourceSystem(notNull("Source system name", systemName));
    }

    private SourceSystemElement sourceSystemByName(String systemName) {
        SourceSystemElement result = findSourceSystemByName(systemName);
        if (result == null) {
            throw new PlatformBusinessException(String.format(SYSTEM_NOT_FOUND_ERROR, systemName), MetaRestExceptionIds.EX_META_SOURCE_SYSTEMS_NOT_FOUND);
        }
        return result;
    }

}
